package nl.hr.minor.mobilelife.j1213.jlr.flow;

import java.util.Arrays;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * A SurfaceView hosting the drawing canvas. Takes care of drawing all {@link Block}s, {@link Line}s, 
 * (re)sizing the {@link Block}s and playing grid. All information is taken from the loaded 
 * {@link FlowLevel}. {@link FlowLevel}s are loaded via the {@link LevelManager}.
 * @author Leon
 *
 */
public class FlowPanel extends SurfaceView implements SurfaceHolder.Callback {

	/**
	 * Thread that takes care of all canvas operations. 
	 */
	private GameThread _mainThread;

	private Block[][] blocks;
	private HostBlock[][] hostBlocks;
	private RectF[] rowRects;
	private Line[] lines;

	private Block curTouchedBlock;
	private Line curLine;

	// statistics
	private float totalBlocksCovered;
	private int currentMovesCount;
	private int totalFlowsCount;
	private int flowsCompleted;
	// end statistics

	private FlowLevel level;
	private int lastSurfaceWidth;
	private IPanelToActivityCallbacks activityCallback;

	private static final String LOG_TAG = FlowPanel.class.getSimpleName();

	public FlowPanel(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context);
	}
	public FlowPanel(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}
	public FlowPanel(Context context) {
		super(context);
		init(context);
	}

	private void init(Context context){

		// setup communication with the activity
		activityCallback = (IPanelToActivityCallbacks) context;

		Block.blockPaint = new Paint();
		Block.blockPaint.setStyle(Paint.Style.STROKE);
		Block.blockPaint.setColor(Color.DKGRAY);
		Block.blockPaint.setStrokeCap(Paint.Cap.SQUARE);
		Block.blockPaint.setStrokeWidth(5);
		
		blocks = new Block[0][0];
		rowRects = new RectF[0];
		lines = new Line[0];
		
		curTouchedBlock = null;
		curLine = null;
		
		totalBlocksCovered = 0;
		currentMovesCount = 0;
		totalFlowsCount = 0;
		flowsCompleted = 0;

		_mainThread = new GameThread(getHolder(), this);
		getHolder().addCallback(this);
	}

	public void onPause() {
		_mainThread.setRunning(false);
	}

	public void onResume() {
		_mainThread.setRunning(true);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		if(blocks != null && lines != null && canvas != null) {

			canvas.drawColor(Color.BLACK);
			for(Block[] row : blocks)
				for(Block b : row)
					b.draw(canvas);

			for(Line l : lines)
				l.draw(canvas);

		} else {
			Log.i(LOG_TAG, "onDraw() skipping onDraw loop, canvas is null");
		}
	}

	public FlowLevel getLevel() {
		return level;
	}

	public void loadLevel(int levelCode) {

		// Pause the thread
		_mainThread.setRunning(false);

		// Get the new Level
		level = LevelManager.getInstance(getContext()).getLevel(levelCode);

		// Run the initialize function
		initializeLevel(level.rowcolCount, lastSurfaceWidth);

		// Done loading. Fire up the thread! 
		_mainThread.setRunning(true);

		// And we have lift-off...
	}

	/**
	 * Generates all Blocks and rowRects. Resizes the dimensions of the Blocks if needed.
	 * @param rowcolCount The amount of Blocks wide/high in the playing grid  
	 * @param newGridSize The total width of the playing grid
	 */
	public void initializeLevel(int rowcolCount, int newGridSize) {

		// Local variables to determine dimensions for the Blocks (only used if a resize is needed)
		float 
		x = 0, 
		y = 0, 
		rib = newGridSize / rowcolCount;

		// We don't like null. 
		// Initialize with the given rowcolCount
		if(blocks == null) blocks = new Block[rowcolCount][rowcolCount];
		if(rowRects == null) rowRects = new RectF[rowcolCount];

		// Determine if a resize of the Blocks dimensions is needed
		// That is, when the rowcolCount or the screenwidth changes
		boolean needsResize = blocks.length != rowcolCount || blocks.length == 0;
		if(needsResize) {
			// 'resize' the row array
			blocks = Arrays.copyOf(blocks, rowcolCount);
			rowRects = Arrays.copyOf(rowRects, rowcolCount);
		}

		// then, 'resize' the column arrays
		for(int i = 0; i < blocks.length; i++) {

			// If the column array is null (happens when the new rowcolCount is bigger than the previous level)
			// fill it with a new Block[]. 
			// If not null, the contents of the old array where kept intact by Arrays.copyOf()
			if(blocks[i] == null)
				blocks[i] = new Block[rowcolCount];
			
			if(rowRects[i] == null) 
				rowRects[i] = new RectF();

			// Reset the x to 0, since a new row starts typically at x=0
			if(needsResize) {
				x = 0;

				// apply the new dimensions to the rowRect of the current row
				rowRects[i].set(x, y, newGridSize, y + rib);
			}

			// If the column array contains a HostBlock or is null, replace it with a new Block instance
			// The HostBlocks will be assigned later
			for(int j = 0; j < blocks[i].length; j++) {
				if(blocks[i][j] instanceof HostBlock || blocks[i][j] == null) {
					blocks[i][j] = new Block(i, j);
				}
				
				// Change the coordinates of the Block if needed
				// Otherwise, the old dimensions stay intact
				if(needsResize) {

					// apply the new dimensions to the Block
					blocks[i][j].set(x, y, x + rib, y + rib);

					// update the x coordinate for the next column
					x += rib;
				}
			}

			// update the y coordinate for the next row
			if(needsResize) y += rib;
		}

		// Apply the HostBlocks
		// Get the HostBlocks from the Level
		hostBlocks = level.getHostBlocks();
		
		// Create the amount of Lines, based on the amount of HostBlock pairs
		lines = new Line[hostBlocks.length];
		
		// Bump out the Block at the position where this HostBlock has to be
		for(int i = 0; i < hostBlocks.length; i++) {

			lines[i] = new Line(hostBlocks[i]);
			lines[i].position = i;

			blocks[hostBlocks[i][0].row][hostBlocks[i][0].column] = hostBlocks[i][0];
			blocks[hostBlocks[i][1].row][hostBlocks[i][1].column] = hostBlocks[i][1];
		}
		
		resizeLevelBlocks(rowcolCount, newGridSize);
		
		// recalculate the stroke width of the Blocks
		Line.LINE_STROKE_WIDTH = rib / 3;
		
		// initialize statistics
		totalBlocksCovered = 0;
		currentMovesCount = 0;
		totalFlowsCount = lines.length;
		flowsCompleted = 0;
		
		onUpdateStatistics();
		onLoadingDone();
	}
	
	/**
	 * Calculates and applies a new size and position to all Blocks based on the given newSize. 
	 * @param newSize Basically the width of the whole screen
	 */
	public void resizeLevelBlocks(int rowcolCount, float newSize) {
		
		// Local variables to determine dimensions for the Blocks
		float rib = newSize / rowcolCount;
		float x = 0, y = 0;

		for(int i = 0; i < blocks.length; i++) {

			x = 0;
			rowRects[i].set(0, y, newSize, y + rib);
			for(int j = 0; j < rowcolCount; j++) {
				blocks[i][j].set(x, y, x + rib, y + rib);
				x += rib;
			}
			y += rib; 
		}
		
		// recalculate the stroke width of the Blocks
		Line.LINE_STROKE_WIDTH = rib / 3;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		final float x = event.getX(), y = event.getY();
		synchronized (getHolder()) {
			switch(event.getAction()){
			case MotionEvent.ACTION_DOWN:

				if(curTouchedBlock == null) {
					// is the action_down on a block?
					Block foundBlock = findBlockAtXY(x, y);
					Log.i("LevelPanel", foundBlock.toString());

					if(foundBlock != null) {
						if(foundBlock instanceof HostBlock) {
							curTouchedBlock = foundBlock;
							curLine = lines[findLineIndexWithBlock(curTouchedBlock)];

							lines[curLine.position].reset();
							lines[curLine.position].setStartHostBlock((HostBlock) foundBlock);
						}
					}
				}

				break;
			case MotionEvent.ACTION_UP:
				
				currentMovesCount++;
				
				Log.i("LevelPanel", "ACTION_UP");
				curTouchedBlock = null;
				curLine = null;

				break;
			case MotionEvent.ACTION_MOVE:

				if(curTouchedBlock != null) {
					// is the action_down on a block?
					Block foundBlock = findBlockAtXY(x, y);

					if(foundBlock != null && curTouchedBlock != null && curLine != null) {
						if(curTouchedBlock.equals(foundBlock)) {
							// curTouchBlock equals foundBlock, nothing to do
							break;
						} else {
							if(canEnterFoundBlock(curLine, curTouchedBlock, foundBlock)){
								Log.i("FlowLevel", "Allowed to enter foundBlock: " + foundBlock);

								curTouchedBlock = foundBlock;

								if(curLine.hasStartHost()) {
									if(foundBlock.equals(curLine.getEndHost())) {
										lines[curLine.position].setHostsConnected(true);
										onLineComplete(curLine.position);
										curLine = null;
										curTouchedBlock = null;
									} else {
										lines[curLine.position].addBlock(foundBlock);
									}
								}
								onUpdateStatistics();
								foundBlock = null;
							} 
						}
					} else {
						Log.i("LevelPanel", "ACTION_MOVE " + foundBlock == null ? "foundBlock == null!" : "" + curTouchedBlock == null ? " curTouchedBlock == null!" : "");
					}
				}

				break;
			default: break;
			}
			return true;
		}
	}

	/**
	 * Called when a line is completed (in other words when two hosts of the same colors are connected)
	 * @param lineIndex The index of the line object in the {@link #lines} array
	 */
	private void onLineComplete(int lineIndex) {
		Log.i("FlowLevel", "onLineComplete() line index: " + lineIndex);

		onUpdateStatistics();

		if(totalBlocksCovered + (lines.length * 2) == level.rowcolCount * level.rowcolCount) {
			onLevelComplete();
		}
	}
	
	/**
	 * Level is complete!
	 */
	private void onLevelComplete() {
		Log.i("FlowLevel", "Level complete!");
		reset();
		activityCallback.onLevelComplete();
	}

	private void onUpdateStatistics() {
		calculateFlowsDone();
		calculatePipePercentageDone();
		activityCallback.onStatisticsUpdate(flowsCompleted, totalFlowsCount, currentMovesCount, totalBlocksCovered);
	}

	private void calculateFlowsDone() {
		int flowsDoneCount = 0;
		for(Line l : lines) {
			if(l.hostsConnected()) flowsDoneCount++;
		}
		flowsCompleted = flowsDoneCount;
	}

	private void calculatePipePercentageDone() {
		int coveredBlocksCount = 0;
		for(Line l : lines) {
			coveredBlocksCount += l.getCoveredBlocks(false);
		}
		totalBlocksCovered = coveredBlocksCount;
	}

	/**
	 * Checks if the current entered block (the toBlock) is able to receive the line the user is drawing.
	 * @param currentLine The line the user is currently drawing
	 * @param fromBlock The block from where the currentLine will come from
	 * @param toBlock The block the user hit by drawing its line
	 * @return True if the currentLine does not already contain the toBlock, the toBlock is not out of
	 * the game boundaries and if the the toBlock is a HostBlock of the currentLine; otherwise false.
	 */
	public boolean canEnterFoundBlock(Line currentLine, Block fromBlock, Block toBlock) {

		// check if the toBlock is not outside the boundaries 
		if((fromBlock.row +1) == toBlock.row && fromBlock.column == toBlock.column){
			// continue
		}else if((fromBlock.row -1) == toBlock.row && fromBlock.column == toBlock.column){
			// continue
		}else if(fromBlock.row == toBlock.row && (fromBlock.column + 1) == toBlock.column){
			// continue
		}else if(fromBlock.row == toBlock.row && (fromBlock.column - 1) == toBlock.column){
			// continue
		}else{
			return false;
		}

		// if the toBlock is an instanceof a HostBlock, it can only enter if it is the same color
		if(toBlock instanceof HostBlock) {
			return toBlock.equals(currentLine.getEndHost());
		} else {
			return !lineContainsBlock(toBlock);
		}
	}

	/**
	 * 
	 * @param block
	 * @return True if one of the lines contains the given Block
	 */
	public boolean lineContainsBlock(Block block) {
		for(Line l : lines) 
			if(l.containsBlock(block))
				return true;
		return false;
	}

	/**
	 * Find the Block at the given x/y coordinate
	 * @param x
	 * @param y
	 * @return The Block found at this x/y coordinate. If non found, null.
	 */
	public Block findBlockAtXY(float x, float y) {
		for(int i = 0; i < rowRects.length; i++)
			if(rowRects[i].contains(x, y))
				for(Block b : blocks[i])
					if(b.contains(x, y))
						return b;
		return null;
	}

	/**
	 * Find the index of the Line this Block belongs to.
	 * @param b
	 * @return The index of the Line this Block belongs to. If no match is found, -1.
	 */
	public int findLineIndexWithBlock(Block b) {
		for(Line l : lines) {
			if(l.containsBlock(b)) 
				return l.position;
		}
		return -1;
	}

	/**
	 * Resets the level. In other words, clears all lines.
	 */
	public void reset() {
		for(Line l : lines) {
			l.reset();
		}
		
		
		curLine = null;
		curTouchedBlock = null;
	}

	private void onLoadingDone() {
		_mainThread.setRunning(true);
		activityCallback.onLevelLoadingDone(level.rowcolCount, level.levelCode);
	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		if(_mainThread.getState() == Thread.State.TERMINATED){
			_mainThread = new GameThread(getHolder(), this);
		}
		_mainThread.setRunning(true);
		_mainThread.start();
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		Log.i(LOG_TAG, "surfaceChanged: width: " + width + ", height: " + height);
		lastSurfaceWidth = width;
		holder.setFixedSize(width, width);
		if(level != null)
			resizeLevelBlocks(level.rowcolCount, width);
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		boolean retry = true;
		_mainThread.setRunning(false);
		while (retry) {
			try {
				_mainThread.join();
				retry = false;
			} catch (InterruptedException e) {
				// we will try it again and again...
			}
		}
	}
}
